﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics.X86;
using System.Security.Policy;
using GlmNet;
using RDF;
using SharpGL;
using SharpGL.SceneGraph.Transformations;
using SharpGL.Shaders;

#if _WIN64
using int_t = System.Int64;
#else
using int_t = System.Int32;
#endif




namespace DZJIFC.Model
{
    public class Scene
    {
        // Shaders
        mat4 _projectionMatrix = mat4.identity();
        private mat4 _modelViewMatrix = mat4.identity();
        private mat3 _normalMatrix = mat3.identity();
        private ShaderProgram _shaderProgram;

        // Selection
        uint _iSelectionFrameBuffer;
        uint _iSelectionTextureBuffer;
        uint _iSelectionDepthRenderBuffer;
        Dictionary<int_t, _color> _dicSelectionColors = new Dictionary<int_t, _color>();
        IInstance _pointedInstance;

        private float XAngle { get; set; } = 315f;
        private float YAngle { get; set; } = 45f;
        private float XTranslation { get; set; } = 0f;
        private float YTranslation { get; set; } = 0f;
        private float ZTranslation { get; set; } = 0f;
        private IController Controller { get; set; }
        private Model Model { get; set; }
        public IInstance SelectedInstance { get; private set; }

        /// <summary>
        /// Selection buffer
        /// </summary>
        private const int SELECTION_BUFFER_SIZE = 512;

        

        #region Methods

        public void Initialize(OpenGL gl, IController controller,Assembly assembly)
        {
            Controller = controller;

            var attributeLocations = new Dictionary<uint, string>
            {
                {0, "Position"},
                {1, "Normal"},
            };

            _shaderProgram = new ShaderProgram();
            _shaderProgram.Create(gl,
                ManifestResourceLoader.LoadTextFile(@"Shaders.Shader.vert", assembly),
                ManifestResourceLoader.LoadTextFile(@"Shaders.Shader.frag", assembly), attributeLocations);

            CreateSelectionBuffer(gl);
        }

        private void CreateSelectionBuffer(OpenGL gl)
        {
            Debug.Assert(_iSelectionFrameBuffer == 0);

            // Frame buffer
            uint[] arFrameBuffers = new uint[1];
            gl.GenFramebuffersEXT(1, arFrameBuffers);

            _iSelectionFrameBuffer = arFrameBuffers[0];
            Debug.Assert(_iSelectionFrameBuffer != 0);

            gl.BindFramebufferEXT(OpenGL.GL_FRAMEBUFFER_EXT, _iSelectionFrameBuffer);

            // Texture buffer
            uint[] arTextures = new uint[1];
            gl.GenTextures(1, arTextures);

            _iSelectionTextureBuffer = arTextures[0];
            Debug.Assert(_iSelectionTextureBuffer != 0);

            gl.BindTexture(OpenGL.GL_TEXTURE_2D, _iSelectionTextureBuffer);
            gl.TexParameter(OpenGL.GL_TEXTURE_2D, OpenGL.GL_TEXTURE_MAG_FILTER, OpenGL.GL_NEAREST);
            gl.TexParameter(OpenGL.GL_TEXTURE_2D, OpenGL.GL_TEXTURE_MIN_FILTER, OpenGL.GL_NEAREST);
            gl.TexParameter(OpenGL.GL_TEXTURE_2D, OpenGL.GL_TEXTURE_WRAP_S, OpenGL.GL_CLAMP_TO_EDGE);
            gl.TexParameter(OpenGL.GL_TEXTURE_2D, OpenGL.GL_TEXTURE_WRAP_T, OpenGL.GL_CLAMP_TO_EDGE);

            gl.TexImage2D(OpenGL.GL_TEXTURE_2D, 0, OpenGL.GL_RGBA, SELECTION_BUFFER_SIZE, SELECTION_BUFFER_SIZE, 0, OpenGL.GL_RGBA, OpenGL.GL_UNSIGNED_BYTE, IntPtr.Zero);

            gl.BindTexture(OpenGL.GL_TEXTURE_2D, 0);

            gl.FramebufferTexture2DEXT(OpenGL.GL_FRAMEBUFFER_EXT, OpenGL.GL_COLOR_ATTACHMENT0_EXT, OpenGL.GL_TEXTURE_2D, _iSelectionTextureBuffer, 0);
            
            // Depth buffer
            uint[] arRenderBuffers = new uint[1];
            gl.GenRenderbuffersEXT(1, arRenderBuffers);

            _iSelectionDepthRenderBuffer = arRenderBuffers[0];
            Debug.Assert(_iSelectionDepthRenderBuffer != 0);

            gl.BindRenderbufferEXT(OpenGL.GL_RENDERBUFFER_EXT, _iSelectionDepthRenderBuffer);
            gl.RenderbufferStorageEXT(OpenGL.GL_RENDERBUFFER_EXT, OpenGL.GL_DEPTH_COMPONENT, SELECTION_BUFFER_SIZE, SELECTION_BUFFER_SIZE);

            gl.BindRenderbufferEXT(OpenGL.GL_RENDERBUFFER_EXT, 0);

            gl.FramebufferRenderbufferEXT(OpenGL.GL_FRAMEBUFFER_EXT, OpenGL.GL_DEPTH_ATTACHMENT_EXT, OpenGL.GL_RENDERBUFFER_EXT, _iSelectionDepthRenderBuffer);

            uint[] arDrawBuffers = new uint[] { OpenGL.GL_COLOR_ATTACHMENT0_EXT };
            gl.DrawBuffers(1, arDrawBuffers);

            gl.BindFramebufferEXT(OpenGL.GL_FRAMEBUFFER_EXT, 0);
        }

        public void Draw(OpenGLControl openGLControl)
        {
            OpenGL gl = openGLControl.OpenGL;

            gl.Viewport(0, 0, openGLControl.Width, openGLControl.Height);

            gl.ClearColor(0.85f, 0.85f, 0.85f, 1f);
            gl.Clear(OpenGL.GL_COLOR_BUFFER_BIT | OpenGL.GL_DEPTH_BUFFER_BIT);

            if (Model == null)
            {
                return;
            }

            gl.Enable(OpenGL.GL_DEPTH_TEST);
            gl.Enable(OpenGL.GL_LEQUAL);

            _shaderProgram.Bind(gl);

            UpdateProjectionMatrix(gl, openGLControl.Width, openGLControl.Height);
            UpdateModelViewAndNormalMatrix();

            _shaderProgram.SetUniform1(gl, "Shininess", 50f);
            _shaderProgram.SetUniform3(gl, "LightPosition", 0.25f, 0.25f, 1f);
            _shaderProgram.SetUniformMatrix4(gl, "ProjectionMatrix", _projectionMatrix.to_array());
            _shaderProgram.SetUniformMatrix4(gl, "ModelViewMatrix", _modelViewMatrix.to_array());
            _shaderProgram.SetUniformMatrix3(gl, "NormalMatrix", _normalMatrix.to_array());

            DrawConcFaces(gl, false);
            DrawConcFaces(gl, true);
            
            DrawConcFacesPolygons(gl);

            DrawLines(gl);

            DrawPoints(gl);

            DrawConcFacesFrameBuffer(gl);

            _shaderProgram.Unbind(gl);
        }

        private void DrawConcFaces(OpenGL gl, bool bTransparent)
        {
            float fScaleFactor = Model.OriginalBoundingSphereDiameter / 2f;
            RDF.engine.GetVertexBufferOffset(Model.Instance, out var dXOffset, out var dYOffset, out var dZOffset);

            float fXTranslation = (float)dXOffset / fScaleFactor;
            float fYTranslation = (float)dYOffset / fScaleFactor;
            float fZTranslation = (float)dZOffset / fScaleFactor;

            if (bTransparent)
            {
                gl.Enable(OpenGL.GL_BLEND);
                gl.BlendEquation(OpenGL.GL_FUNC_ADD_EXT);
                gl.BlendFunc(OpenGL.GL_SRC_ALPHA, OpenGL.GL_ONE_MINUS_SRC_ALPHA);
            }

            foreach (var prGeometry in Model.Geometries)
            {
                var geometry = prGeometry.Value;
                if (!geometry.HasGeometry)
                {
                    continue;
                }

                foreach (var instance in geometry.Instances)
                {
                    if (!instance.Enabled)
                    {
                        continue;
                    }

                    if ((instance == _pointedInstance) || (instance == SelectedInstance))
                    {
                        continue;
                    }

                    // Transformation
                    var transformation = mat4.identity();
                    if (instance.Transformation != null)
                    {
                        transformation = _matrix4x4.ToMat4(instance.Transformation);
                    }

                    var modelViewMatrix = glm.translate(_modelViewMatrix, new vec3(fXTranslation, fYTranslation, fZTranslation));
                    modelViewMatrix = modelViewMatrix * transformation;
                    modelViewMatrix = glm.translate(modelViewMatrix, new vec3(-fXTranslation, -fYTranslation, -fZTranslation));
                    var normalMatrix = modelViewMatrix.to_mat3();
                    _shaderProgram.SetUniformMatrix4(gl, "ModelViewMatrix", modelViewMatrix.to_array());
                    _shaderProgram.SetUniformMatrix3(gl, "NormalMatrix", normalMatrix.to_array());

                    foreach (var cohort in instance.Geometry.ConcFacesVerticesCohorts)
                    {
                        if (bTransparent)
                        {
                            if (cohort.Material.A == 1.0)
                            {
                                continue;
                            }
                        }
                        else
                        {
                            if (cohort.Material.A < 1.0)
                            {
                                continue;
                            }
                        }

                        _shaderProgram.SetUniform3(gl, "DiffuseMaterial",
                            cohort.Material.Diffuse.R,
                            cohort.Material.Diffuse.G,
                            cohort.Material.Diffuse.B);

                        _shaderProgram.SetUniform3(gl, "AmbientMaterial",
                           cohort.Material.Ambient.R,
                           cohort.Material.Ambient.G,
                           cohort.Material.Ambient.B);

                        _shaderProgram.SetUniform3(gl, "SpecularMaterial",
                           cohort.Material.Specular.R,
                           cohort.Material.Specular.G,
                           cohort.Material.Specular.B);

                        _shaderProgram.SetUniform1(gl, "Transparency", cohort.Material.A);

                        cohort.VertexBufferArray.Bind(gl);

                        gl.DrawElements(OpenGL.GL_TRIANGLES, cohort.IndicesCount, OpenGL.GL_UNSIGNED_SHORT, IntPtr.Zero);

                        cohort.VertexBufferArray.Unbind(gl);
                    }

                    UpdateModelViewAndNormalMatrix();
                } // foreach (var instance in ...
            } // foreach (var prGeometry in ...

            if (bTransparent)
            {
                DrawPointedInstance(gl);

                gl.Disable(OpenGL.GL_BLEND);
            }
            else
            {
                DrawSelectedInstance(gl);
            }
        }

        private void DrawPointedInstance(OpenGL gl)
        {
            if ((_pointedInstance == null) || (_pointedInstance == SelectedInstance))
            {
                return;
            }

            float fScaleFactor = Model.OriginalBoundingSphereDiameter / 2f;
            RDF.engine.GetVertexBufferOffset(Model.Instance, out var dXOffset, out var dYOffset, out var dZOffset);

            float fXTranslation = (float)dXOffset / fScaleFactor;
            float fYTranslation = (float)dYOffset / fScaleFactor;
            float fZTranslation = (float)dZOffset / fScaleFactor;

            // Transformation
            var transformation = mat4.identity();
            if (_pointedInstance.Transformation != null)
            {
                transformation = _matrix4x4.ToMat4(_pointedInstance.Transformation);
            }

            var modelViewMatrix = glm.translate(_modelViewMatrix, new vec3(fXTranslation, fYTranslation, fZTranslation));
            modelViewMatrix = modelViewMatrix * transformation;
            modelViewMatrix = glm.translate(modelViewMatrix, new vec3(-fXTranslation, -fYTranslation, -fZTranslation));
            var normalMatrix = modelViewMatrix.to_mat3();
            _shaderProgram.SetUniformMatrix4(gl, "ModelViewMatrix", modelViewMatrix.to_array());
            _shaderProgram.SetUniformMatrix3(gl, "NormalMatrix", normalMatrix.to_array());

            var material = _material.GetPointedInstanceMaterial();

            _shaderProgram.SetUniform3(gl, "DiffuseMaterial",
                material.Diffuse.R,
                material.Diffuse.G,
                material.Diffuse.B);

            _shaderProgram.SetUniform3(gl, "AmbientMaterial",
               material.Ambient.R,
               material.Ambient.G,
               material.Ambient.B);

            _shaderProgram.SetUniform3(gl, "SpecularMaterial",
               material.Specular.R,
               material.Specular.G,
               material.Specular.B);

            _shaderProgram.SetUniform1(gl, "Transparency", material.A);            

            foreach (var cohort in _pointedInstance.Geometry.ConcFacesVerticesCohorts)
            {               
                cohort.VertexBufferArray.Bind(gl);

                gl.DrawElements(OpenGL.GL_TRIANGLES, cohort.IndicesCount, OpenGL.GL_UNSIGNED_SHORT, IntPtr.Zero);

                cohort.VertexBufferArray.Unbind(gl);
            }

            UpdateModelViewAndNormalMatrix();
        }

        private void DrawSelectedInstance(OpenGL gl)
        {
            if (SelectedInstance == null)
            {
                return;
            }

            float fScaleFactor = Model.OriginalBoundingSphereDiameter / 2f;
            RDF.engine.GetVertexBufferOffset(Model.Instance, out var dXOffset, out var dYOffset, out var dZOffset);

            float fXTranslation = (float)dXOffset / fScaleFactor;
            float fYTranslation = (float)dYOffset / fScaleFactor;
            float fZTranslation = (float)dZOffset / fScaleFactor;

            // Transformation
            var transformation = mat4.identity();
            if (SelectedInstance.Transformation != null)
            {
                transformation = _matrix4x4.ToMat4(SelectedInstance.Transformation);
            }

            var modelViewMatrix = glm.translate(_modelViewMatrix, new vec3(fXTranslation, fYTranslation, fZTranslation));
            modelViewMatrix = modelViewMatrix * transformation;
            modelViewMatrix = glm.translate(modelViewMatrix, new vec3(-fXTranslation, -fYTranslation, -fZTranslation));
            var normalMatrix = modelViewMatrix.to_mat3();
            _shaderProgram.SetUniformMatrix4(gl, "ModelViewMatrix", modelViewMatrix.to_array());
            _shaderProgram.SetUniformMatrix3(gl, "NormalMatrix", normalMatrix.to_array());

            var material = _material.GetSelectedInstanceMaterial();

            _shaderProgram.SetUniform3(gl, "DiffuseMaterial",
                material.Diffuse.R,
                material.Diffuse.G,
                material.Diffuse.B);

            _shaderProgram.SetUniform3(gl, "AmbientMaterial",
               material.Ambient.R,
               material.Ambient.G,
               material.Ambient.B);

            _shaderProgram.SetUniform3(gl, "SpecularMaterial",
               material.Specular.R,
               material.Specular.G,
               material.Specular.B);

            _shaderProgram.SetUniform1(gl, "Transparency", material.A);

            foreach (var cohort in SelectedInstance.Geometry.ConcFacesVerticesCohorts)
            {
                cohort.VertexBufferArray.Bind(gl);

                gl.DrawElements(OpenGL.GL_TRIANGLES, cohort.IndicesCount, OpenGL.GL_UNSIGNED_SHORT, IntPtr.Zero);

                cohort.VertexBufferArray.Unbind(gl);
            }

            UpdateModelViewAndNormalMatrix();
        }

        private void DrawConcFacesPolygons(OpenGL gl)
        {
            float fScaleFactor = Model.OriginalBoundingSphereDiameter / 2f;
            RDF.engine.GetVertexBufferOffset(Model.Instance, out var dXOffset, out var dYOffset, out var dZOffset);

            float fXTranslation = (float)dXOffset / fScaleFactor;
            float fYTranslation = (float)dYOffset / fScaleFactor;
            float fZTranslation = (float)dZOffset / fScaleFactor;

            foreach (var prGeometry in Model.Geometries)
            {
                var geometry = prGeometry.Value;
                if (!geometry.HasGeometry)
                {
                    continue;
                }

                foreach (var instance in geometry.Instances)
                {
                    if (!instance.Enabled)
                    {
                        continue;
                    }

                    // Transformation
                    var transformation = mat4.identity();
                    if (instance.Transformation != null)
                    {
                        transformation = _matrix4x4.ToMat4(instance.Transformation);
                    }

                    var modelViewMatrix = glm.translate(_modelViewMatrix, new vec3(fXTranslation, fYTranslation, fZTranslation));
                    modelViewMatrix = modelViewMatrix * transformation;
                    modelViewMatrix = glm.translate(modelViewMatrix, new vec3(-fXTranslation, -fYTranslation, -fZTranslation));
                    var normalMatrix = modelViewMatrix.to_mat3();
                    _shaderProgram.SetUniformMatrix4(gl, "ModelViewMatrix", modelViewMatrix.to_array());
                    _shaderProgram.SetUniformMatrix3(gl, "NormalMatrix", normalMatrix.to_array());

                    _shaderProgram.SetUniform3(gl, "DiffuseMaterial", 0f, 0f, 0f);
                    _shaderProgram.SetUniform3(gl, "AmbientMaterial", 0f, 0f, 0f);
                    _shaderProgram.SetUniform3(gl, "SpecularMaterial", 0f, 0f, 0f);
                    _shaderProgram.SetUniform1(gl, "Transparency", 1f);

                    foreach (var cohort in instance.Geometry.ConcFacePolygonsVerticesCohorts)
                    {
                        cohort.VertexBufferArray.Bind(gl);

                        gl.DrawElements(OpenGL.GL_LINES, cohort.IndicesCount, OpenGL.GL_UNSIGNED_SHORT, IntPtr.Zero);

                        cohort.VertexBufferArray.Unbind(gl);
                    }

                    UpdateModelViewAndNormalMatrix();
                } // foreach (var instance in ...
            } // foreach (var prGeometry in ...
        }

        private void DrawLines(OpenGL gl)
        {
            float fScaleFactor = Model.OriginalBoundingSphereDiameter / 2f;
            RDF.engine.GetVertexBufferOffset(Model.Instance, out var dXOffset, out var dYOffset, out var dZOffset);

            float fXTranslation = (float)dXOffset / fScaleFactor;
            float fYTranslation = (float)dYOffset / fScaleFactor;
            float fZTranslation = (float)dZOffset / fScaleFactor;

            foreach (var prGeometry in Model.Geometries)
            {
                var geometry = prGeometry.Value;
                if (!geometry.HasGeometry)
                {
                    continue;
                }

                foreach (var instance in geometry.Instances)
                {
                    if (!instance.Enabled)
                    {
                        continue;
                    }

                    // Transformation
                    var transformation = mat4.identity();
                    if (instance.Transformation != null)
                    {
                        transformation = _matrix4x4.ToMat4(instance.Transformation);
                    }

                    var modelViewMatrix = glm.translate(_modelViewMatrix, new vec3(fXTranslation, fYTranslation, fZTranslation));
                    modelViewMatrix = modelViewMatrix * transformation;
                    modelViewMatrix = glm.translate(modelViewMatrix, new vec3(-fXTranslation, -fYTranslation, -fZTranslation));
                    var normalMatrix = modelViewMatrix.to_mat3();
                    _shaderProgram.SetUniformMatrix4(gl, "ModelViewMatrix", modelViewMatrix.to_array());
                    _shaderProgram.SetUniformMatrix3(gl, "NormalMatrix", normalMatrix.to_array());

                    _shaderProgram.SetUniform3(gl, "DiffuseMaterial", 0f, 0f, 0f);
                    _shaderProgram.SetUniform3(gl, "AmbientMaterial", 0f, 0f, 0f);
                    _shaderProgram.SetUniform3(gl, "SpecularMaterial", 0f, 0f, 0f);
                    _shaderProgram.SetUniform1(gl, "Transparency", 1f);

                    foreach (var cohort in instance.Geometry.LinesVerticesCohorts)
                    {
                        _shaderProgram.SetUniform3(gl, "DiffuseMaterial",
                            cohort.Material.Diffuse.R,
                            cohort.Material.Diffuse.G,
                            cohort.Material.Diffuse.B);

                        _shaderProgram.SetUniform3(gl, "AmbientMaterial",
                           cohort.Material.Ambient.R,
                           cohort.Material.Ambient.G,
                           cohort.Material.Ambient.B);

                        _shaderProgram.SetUniform3(gl, "SpecularMaterial",
                           cohort.Material.Specular.R,
                           cohort.Material.Specular.G,
                           cohort.Material.Specular.B);

                        _shaderProgram.SetUniform1(gl, "Transparency", cohort.Material.A);

                        cohort.VertexBufferArray.Bind(gl);

                        gl.DrawElements(OpenGL.GL_LINES, cohort.IndicesCount, OpenGL.GL_UNSIGNED_SHORT, IntPtr.Zero);

                        cohort.VertexBufferArray.Unbind(gl);
                    }

                    UpdateModelViewAndNormalMatrix();
                } // foreach (var instance in ...
            } // foreach (var prGeometry in ...
        }

        private void DrawPoints(OpenGL gl)
        {
            float fScaleFactor = Model.OriginalBoundingSphereDiameter / 2f;
            RDF.engine.GetVertexBufferOffset(Model.Instance, out var dXOffset, out var dYOffset, out var dZOffset);

            float fXTranslation = (float)dXOffset / fScaleFactor;
            float fYTranslation = (float)dYOffset / fScaleFactor;
            float fZTranslation = (float)dZOffset / fScaleFactor;

            foreach (var prGeometry in Model.Geometries)
            {
                var geometry = prGeometry.Value;
                if (!geometry.HasGeometry)
                {
                    continue;
                }

                foreach (var instance in geometry.Instances)
                {
                    if (!instance.Enabled)
                    {
                        continue;
                    }

                    // Transformation
                    var transformation = mat4.identity();
                    if (instance.Transformation != null)
                    {
                        transformation = _matrix4x4.ToMat4(instance.Transformation);
                    }

                    var modelViewMatrix = glm.translate(_modelViewMatrix, new vec3(fXTranslation, fYTranslation, fZTranslation));
                    modelViewMatrix = modelViewMatrix * transformation;
                    modelViewMatrix = glm.translate(modelViewMatrix, new vec3(-fXTranslation, -fYTranslation, -fZTranslation));
                    var normalMatrix = modelViewMatrix.to_mat3();
                    _shaderProgram.SetUniformMatrix4(gl, "ModelViewMatrix", modelViewMatrix.to_array());
                    _shaderProgram.SetUniformMatrix3(gl, "NormalMatrix", normalMatrix.to_array());

                    foreach (var cohort in instance.Geometry.PointsVerticesCohorts)
                    {
                        _shaderProgram.SetUniform3(gl, "DiffuseMaterial",
                            cohort.Material.Diffuse.R,
                            cohort.Material.Diffuse.G,
                            cohort.Material.Diffuse.B);

                        _shaderProgram.SetUniform3(gl, "AmbientMaterial",
                           cohort.Material.Ambient.R,
                           cohort.Material.Ambient.G,
                           cohort.Material.Ambient.B);

                        _shaderProgram.SetUniform3(gl, "SpecularMaterial",
                           cohort.Material.Specular.R,
                           cohort.Material.Specular.G,
                           cohort.Material.Specular.B);

                        _shaderProgram.SetUniform1(gl, "Transparency", cohort.Material.A);

                        cohort.VertexBufferArray.Bind(gl);

                        gl.DrawElements(OpenGL.GL_POINTS, cohort.IndicesCount, OpenGL.GL_UNSIGNED_SHORT, IntPtr.Zero);

                        cohort.VertexBufferArray.Unbind(gl);
                    }

                    UpdateModelViewAndNormalMatrix();
                } // foreach (var instance in ...
            } // foreach (var prGeometry in ...
        }

        private void DrawConcFacesFrameBuffer(OpenGL gl)
        {
            if (Model == null)
            {
                return;
            }

            Debug.Assert(_iSelectionFrameBuffer != 0);

            if (_dicSelectionColors.Count == 0)
            {
                foreach (var prGeometry in Model.Geometries)
                {
                    var geometry = prGeometry.Value;
                    if (!geometry.HasGeometry)
                    {
                        continue;
                    }

                    foreach (var instance in geometry.Instances)
                    {
                        float fR, fG, fB;
                        _i64RGBCoder.encode(instance.ID, out fR, out fG, out fB);

                        Debug.Assert(!_dicSelectionColors.ContainsKey(instance.ID));
                        _dicSelectionColors[instance.ID] = new _color(fR, fG, fB, 1f);
                    } // foreach (var instance in ...
                } // foreach (var prGeometry in ...
            }

            gl.BindFramebufferEXT(OpenGL.GL_FRAMEBUFFER_EXT, _iSelectionFrameBuffer);

            gl.Viewport(0, 0, SELECTION_BUFFER_SIZE, SELECTION_BUFFER_SIZE);

            gl.ClearColor(0f, 0f, 0f, 0f);
            gl.Clear(OpenGL.GL_COLOR_BUFFER_BIT | OpenGL.GL_DEPTH_BUFFER_BIT);

            gl.Enable(OpenGL.GL_DEPTH_TEST);
            gl.Enable(OpenGL.GL_LEQUAL);

            _shaderProgram.SetUniform1(gl, "EnableLighting", 0f);

            float fScaleFactor = Model.OriginalBoundingSphereDiameter / 2f;
            RDF.engine.GetVertexBufferOffset(Model.Instance, out var dXOffset, out var dYOffset, out var dZOffset);

            float fXTranslation = (float)dXOffset / fScaleFactor;
            float fYTranslation = (float)dYOffset / fScaleFactor;
            float fZTranslation = (float)dZOffset / fScaleFactor;

            foreach (var prGeometry in Model.Geometries)
            {
                var geometry = prGeometry.Value;
                if (!geometry.HasGeometry)
                {
                    continue;
                }

                foreach (var instance in geometry.Instances)
                {
                    if (!instance.Enabled)
                    {
                        continue;
                    }

                    // Transformation
                    var transformation = mat4.identity();
                    if (instance.Transformation != null)
                    {
                        transformation = _matrix4x4.ToMat4(instance.Transformation);
                    }

                    var modelViewMatrix = glm.translate(_modelViewMatrix, new vec3(fXTranslation, fYTranslation, fZTranslation));
                    modelViewMatrix = modelViewMatrix * transformation;
                    modelViewMatrix = glm.translate(modelViewMatrix, new vec3(-fXTranslation, -fYTranslation, -fZTranslation));
                    var normalMatrix = modelViewMatrix.to_mat3();
                    _shaderProgram.SetUniformMatrix4(gl, "ModelViewMatrix", modelViewMatrix.to_array());
                    _shaderProgram.SetUniformMatrix3(gl, "NormalMatrix", normalMatrix.to_array());

                    Debug.Assert(_dicSelectionColors.ContainsKey(instance.ID));
                    var selectionColor = _dicSelectionColors[instance.ID];

                    _shaderProgram.SetUniform3(gl, "AmbientMaterial",
                           selectionColor.R,
                           selectionColor.G,
                           selectionColor.B);

                    foreach (var cohort in instance.Geometry.ConcFacesVerticesCohorts)
                    {
                        cohort.VertexBufferArray.Bind(gl);

                        gl.DrawElements(OpenGL.GL_TRIANGLES, cohort.IndicesCount, OpenGL.GL_UNSIGNED_SHORT, IntPtr.Zero);

                        cohort.VertexBufferArray.Unbind(gl);
                    }

                    UpdateModelViewAndNormalMatrix();
                } // foreach (var instance ...
            } // foreach (var prGeometry in ...

            _shaderProgram.SetUniform1(gl, "EnableLighting", 1f);

            gl.BindFramebufferEXT(OpenGL.GL_FRAMEBUFFER_EXT, 0);
        }

        public void OnModelLoaded(Model model)
        {
            Model = model;

            _dicSelectionColors.Clear();
            _pointedInstance = null;
            SelectedInstance = null;

            XTranslation = 0f;
            YTranslation = 0f;
            ZTranslation = 0f;

            if (Model == null)
            {
                return;
            }

            XTranslation = -(Model.Xmin + Model.Xmax) / 2f;
            YTranslation = -(Model.Ymin + Model.Ymax) / 2f;
            ZTranslation = -(Model.Zmin + Model.Zmax) / 2f;
            ZTranslation -= Model.BoundingSphereDiameter * 2f;
        }

        public void OnInstanceSelected(object sender, IInstance instance)
        {
            if (sender == this)
            {
                return;
            }

            SelectedInstance = instance;
        }

        public void OnWorldDimensionsChanged()
        {
            XTranslation = -(Model.Xmin + Model.Xmax) / 2f;
            YTranslation = -(Model.Ymin + Model.Ymax) / 2f;
            ZTranslation = -(Model.Zmin + Model.Zmax) / 2f;
            ZTranslation -= Model.BoundingSphereDiameter * 2f;
        }

        public void OnResize(OpenGL gl, float fWidth, float fHeight)
        {
            UpdateProjectionMatrix(gl, fWidth, fHeight);
        }

        public void Rotate(float fXAngle, float fYAngle)
        {
            XAngle += fXAngle;
            if (XAngle > 360.0)
            {
                XAngle = XAngle - 360.0f;
            }
            else
            {
                if (XAngle < 0.0)
                {
                    XAngle = XAngle + 360.0f;
                }
            }

            YAngle += fYAngle;
            if (YAngle > 360.0)
            {
                YAngle = YAngle - 360.0f;
            }
            else
            {
                if (YAngle < 0.0)
                {
                    YAngle = YAngle + 360.0f;
                }
            }
        }

        public void Move(float fXTranslation, float fYTranslation)
        {
            XTranslation += fXTranslation;
            YTranslation += fYTranslation;
        }

        public void Zoom(float fZTranslation)
        {
            ZTranslation += fZTranslation;
        }

        public void Zoom(bool bIn)
        {
            if (bIn)
            {
                ZTranslation += Model.BoundingSphereDiameter * 0.005f;
            }
            else
            {
                ZTranslation -= Model.BoundingSphereDiameter * 0.005f;
            }
        }

        private void UpdateProjectionMatrix(OpenGL gl, float fWidth, float fHeight)
        {
            // fovY     - Field of vision in degrees in the y direction
            // aspect   - Aspect ratio of the viewport
            // zNear    - The near clipping distance
            // zFar     - The far clipping distance
            double fovY = 45.0;
            double aspect = (double)fWidth / (double)fHeight;
            double zNear = 0.01;
            double zFar = 1000.0;

            double fH = Math.Tan(fovY / 360 * Math.PI) * zNear;
            double fW = fH * aspect;

            _projectionMatrix = glm.frustum((float)-fW, (float)fW, (float)-fH, (float)fH, (float)zNear, (float)zFar);

            gl.MatrixMode(OpenGL.GL_PROJECTION);
            gl.LoadIdentity();
            gl.MultMatrix(_projectionMatrix.to_array());
            gl.MatrixMode(OpenGL.GL_MODELVIEW);
        }

        private void UpdateModelViewAndNormalMatrix()
        {
            if (Model == null)
            {
                return;
            }

            float fXTranslation = Model.Xmin;
            fXTranslation += (Model.Xmax - Model.Xmin) / 2.0f;
            fXTranslation = -fXTranslation;

            float fYTranslation = Model.Ymin;
            fYTranslation += (Model.Ymax - Model.Ymin) / 2.0f;
            fYTranslation = -fYTranslation;

            float fZTranslation = Model.Zmin;
            fZTranslation += (Model.Zmax - Model.Zmin) / 2.0f;
            fZTranslation = -fZTranslation;

            _modelViewMatrix = glm.translate(mat4.identity(), new vec3(XTranslation, YTranslation, ZTranslation));
            _modelViewMatrix = glm.translate(_modelViewMatrix, new vec3(-fXTranslation, -fYTranslation, -fZTranslation));
            _modelViewMatrix = glm.rotate(_modelViewMatrix, glm.radians(XAngle), new vec3(1, 0, 0));
            _modelViewMatrix = glm.rotate(_modelViewMatrix, glm.radians(YAngle), new vec3(0, 0, 1));
            _modelViewMatrix = glm.translate(_modelViewMatrix, new vec3(fXTranslation, fYTranslation, fZTranslation));

            _normalMatrix = _modelViewMatrix.to_mat3();
        }

        public void PointInstance(OpenGLControl openGLControl, Point point, bool bInteractionInProgress)
        {
            if (bInteractionInProgress)
            {
                _pointedInstance = null;

                return;
            }

            _pointedInstance = GetPointedInstance(openGLControl, point);
        }

        public void SelectInstance(OpenGLControl openGLControl, Point point)
        {
            if (SelectedInstance == _pointedInstance) 
            { 
                return; 
            }

            SelectedInstance = _pointedInstance;

            Controller.SelectInstance(this, SelectedInstance);
        }

        private IInstance GetPointedInstance(OpenGLControl openGLControl, Point point)
        {
            if ((Model == null) || _dicSelectionColors.Count == 0)
            {
                return null;
            }

            Debug.Assert(_iSelectionFrameBuffer != 0);

            OpenGL gl = openGLControl.OpenGL;

            double dX = (double)point.X * ((double)SELECTION_BUFFER_SIZE / (double)openGLControl.Width);
            double dY = ((double)openGLControl.Height - (double)point.Y) * ((double)SELECTION_BUFFER_SIZE / (double)openGLControl.Height);

            gl.BindFramebufferEXT(OpenGL.GL_FRAMEBUFFER_EXT, _iSelectionFrameBuffer);

            byte[] arPixels = new byte[4];
            gl.ReadPixels(
                (int)dX,
                (int)dY,
                1, 1,
                OpenGL.GL_RGBA,
                OpenGL.GL_UNSIGNED_BYTE,
                arPixels);

            gl.BindFramebufferEXT(OpenGL.GL_FRAMEBUFFER_EXT, 0);

            IInstance pointedInstance = null;
            if (arPixels[3] != 0)
            {
                int_t iInstanceID = _i64RGBCoder.decode(arPixels[0], arPixels[1], arPixels[2]);

                Debug.Assert(Model.Instances.ContainsKey(iInstanceID));
                pointedInstance = Model.Instances[iInstanceID];
            } // if (arPixels[3] != 0)           

            return pointedInstance;
        }

        public IInstance GetSelectedInstance(OpenGLControl openGLControl, Point point)
        {
            if (Model == null)
            {
                return null;
            }

            Debug.Assert(_iSelectionFrameBuffer != 0);

            OpenGL gl = openGLControl.OpenGL;

            double dX = (double)point.X * ((double)SELECTION_BUFFER_SIZE / (double)openGLControl.Width);
            double dY = ((double)openGLControl.Height - (double)point.Y) * ((double)SELECTION_BUFFER_SIZE / (double)openGLControl.Height);

            gl.BindFramebufferEXT(OpenGL.GL_FRAMEBUFFER_EXT, _iSelectionFrameBuffer);

            byte[] arPixels = new byte[4];
            gl.ReadPixels(
                (int)dX,
                (int)dY,
                1, 1,
                OpenGL.GL_RGBA,
                OpenGL.GL_UNSIGNED_BYTE,
                arPixels);
            
            gl.BindFramebufferEXT(OpenGL.GL_FRAMEBUFFER_EXT, 0);

            IInstance selectedInstance = null;
            if (arPixels[3] != 0)
            {
                int_t iInstanceID = _i64RGBCoder.decode(arPixels[0], arPixels[1], arPixels[2]);

                Debug.Assert(Model.Instances.ContainsKey(iInstanceID));
                selectedInstance = Model.Instances[iInstanceID];
            } // if (arPixels[3] != 0)           

            return selectedInstance;
        }

        #endregion // Methods


    }
}
